//===============================================================================
// Microsoft patterns & practices
// Mobile Client Software Factory - July 2006
//===============================================================================
// Copyright  Microsoft Corporation.  All rights reserved.
// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY
// OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT
// LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
// FITNESS FOR A PARTICULAR PURPOSE.
//===============================================================================
// The example companies, organizations, products, domain names,
// e-mail addresses, logos, people, places, and events depicted
// herein are fictitious.  No association with any real company,
// organization, product, domain name, email address, logo, person,
// places, or events is intended or should be inferred.
//===============================================================================

using System;
using System.Collections.Generic;
using System.Text;
using System.CodeDom;
using System.CodeDom.Compiler;
using Microsoft.Practices.Mobile.ObjectBuilder;
using System.IO;

namespace Microsoft.Practices.ObjectBuilder.ObGen
{
	public class PolicyProviderGenerator
	{
		private CodeCompileUnit unit = new CodeCompileUnit();

		public PolicyProviderGenerator()
		{
			unit.ReferencedAssemblies.Add(Path.GetFileName(new Uri(typeof(IPolicyProvider).Assembly.CodeBase).LocalPath));
		}

		/// <summary>
		/// Gets the compile unit generated so far.
		/// </summary>
		public CodeCompileUnit CompileUnit
		{
			get  { return unit; }
		}

		/// <summary>
		/// Builds a provider for the <paramref name="typeToProcess"/>, and returns the 
		/// full name of the generated provider, or <see langword="null" /> if no type 
		/// was generated (i.e. no code was emitted by any of the provided generators).
		/// </summary>
		public string AddProvider(Type typeToProcess, IEnumerable<IPolicyGenerator> generators)
		{
			CodeNamespace ns = FindOrCreateNamespace(GetProviderNamespace(typeToProcess));
			ns.Imports.Add(new CodeNamespaceImport("System"));
			CodeTypeDeclaration provider;
			CodeMemberMethod getPolicies;
			CreateProvider(typeToProcess, out provider, out getPolicies);
			
			// We don't add the type to the namespace yet, as we may not need it in the end.
			CodeStatementCollection statements = new CodeStatementCollection();
			NamingContext context = new NamingContext();

			foreach (IPolicyGenerator generator in generators)
			{
				CodeStatementCollection code = generator.GenerateStatements(typeToProcess, provider, context);
				if (code != null && code.Count > 0)
				{
					statements.Add(new CodeCommentStatement(String.Empty));
					statements.Add(new CodeCommentStatement(generator.ToString()));
					statements.Add(new CodeCommentStatement(String.Empty));
					statements.AddRange(code);

					AddAssemblyReferences(generator);
					AddNamespaceImports(ns, generator);
				}
			}

			if (statements.Count != 0)
			{
				getPolicies.Statements.AddRange(statements);
				getPolicies.Statements.Add(new CodeMethodReturnStatement(new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), "policies")));
				// We do have some code, so add the type to the namespace.
				ns.Types.Add(provider);

				return ns.Name + "." + provider.Name;
			}
			else
			{
				return null;
			}
		}

		private void AddAssemblyReferences(IPolicyGenerator generator)
		{
			foreach (string assembly in generator.AssemblyReferences)
			{
				if (!CompileUnit.ReferencedAssemblies.Contains(assembly))
				{
					CompileUnit.ReferencedAssemblies.Add(assembly);
				}
			}
		}

		private static void AddNamespaceImports(CodeNamespace ns, IPolicyGenerator generator)
		{
			foreach (string nsimport in generator.NamespaceImports)
			{
				// Duplicated imports are harmless.
				ns.Imports.Add(new CodeNamespaceImport(nsimport));
			}
		}

		private CodeNamespace FindOrCreateNamespace(string targetNs)
		{
			foreach (CodeNamespace ns in CompileUnit.Namespaces)
			{
				if (ns.Name == targetNs)
				{
					return ns;
				}
			}

			CodeNamespace codeNs = new CodeNamespace(targetNs);
			CompileUnit.Namespaces.Add(codeNs);
			codeNs.Imports.Add(new CodeNamespaceImport(typeof(PolicyList).Namespace));
			codeNs.Imports.Add(new CodeNamespaceImport(typeof(IPolicyProvider).Namespace));

			return codeNs;
		}

		/// <summary>
		/// Gets the namespace of the provider. By default, it will be the same namespace as the <paramref name="typeToProcess"/>.
		/// </summary>
		protected virtual string GetProviderNamespace(Type typeToProcess)
		{
			return typeToProcess.Namespace;
		}

		/// <summary>
		/// Gets the type name of the provider. By default, it will be the <see cref="Type.Name"/> of the <paramref name="typeToProcess"/> 
		/// with the <see cref="Properties.Resources.PolicyProviderTypeNameSuffix"/> suffix appended.
		/// </summary>
		protected virtual string GetProviderName(Type typeToProcess)
		{
			return typeToProcess.Name + Properties.Resources.PolicyProviderTypeNameSuffix;
		}

		/// <summary>
		/// Allows derived generators to inject additional members on the provider, 
		/// or add a base class.
		/// </summary>
		/// <param name="provider"></param>
		protected virtual void OnProviderCreated(CodeTypeDeclaration provider)
		{
		}

		/// <summary>
		/// Creates the type declaration for the provider. The generated type must implement the 
		/// <see cref="IPolicyProvider"/> interface.
		/// </summary>
		/// <remarks>
		/// Default provider exposes a <c>policies</c> field within the method, as well as 
		/// a <c>Policies</c> protected property that policy injectors can use to 
		/// add policies.
		/// </remarks>
		private void CreateProvider(Type typeToProcess, out CodeTypeDeclaration provider, out CodeMemberMethod getPolicies)
		{
			provider = new CodeTypeDeclaration(GetProviderName(typeToProcess));

			OnProviderCreated(provider);

			provider.BaseTypes.Add(typeof(IPolicyProvider).Name);

			// public PolicyList GetPolicies(PolicyList existingPolicies, Type typeToBuild, string idToBuild) { }
			getPolicies = new CodeMemberMethod();
			provider.Members.Add(getPolicies);
			getPolicies.Attributes = MemberAttributes.Public | MemberAttributes.Final;

			// Ensures the method exists or will fail at codegen time for tests, even without compiling.
			getPolicies.Name = typeof(IPolicyProvider).GetMethod("GetPolicies").Name; 
			getPolicies.ReturnType = new SafeCodeTypeReference(typeof(PolicyList));
			// Need to add the implementation types for VB :S.
			getPolicies.ImplementationTypes.Add(typeof(IPolicyProvider).Name);
			getPolicies.Parameters.Add(new CodeParameterDeclarationExpression(typeof(PolicyList), "existingPolicies"));
			getPolicies.Parameters.Add(new CodeParameterDeclarationExpression(typeof(Type), "typeToBuild"));
			getPolicies.Parameters.Add(new CodeParameterDeclarationExpression(typeof(string), "idToBuild"));

			// if (existingPolicies == null) throw new ArgumentNullException("existingPolicies");
			getPolicies.Statements.Add(ThrowIfNullArgument("existingPolicies"));
			// if (typeToBuild == null) throw new ArgumentNullException("typeToBuild");
			getPolicies.Statements.Add(ThrowIfNullArgument("typeToBuild"));

			// private PolicyList policies;
			provider.Members.Add(new CodeMemberField(typeof(PolicyList).Name, "policies"));

			// protected PolicyList Policies { get { return this.policies; } }
			CodeMemberProperty policies = new CodeMemberProperty();
			provider.Members.Add(policies);
			policies.Name = "Policies";
			policies.Attributes = MemberAttributes.Family | MemberAttributes.Final;
			policies.Type = new SafeCodeTypeReference(typeof(PolicyList));
			policies.GetStatements.Add(
				new CodeMethodReturnStatement(
					new CodeFieldReferenceExpression(
						new CodeThisReferenceExpression(),
						"policies")));

			// this.policies = new PolicyList();
			getPolicies.Statements.Add(new CodeAssignStatement(
				new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), "policies"), 
				new CodeObjectCreateExpression(typeof(PolicyList).Name)));
		}

		private CodeStatement ThrowIfNullArgument(string argumentName)
		{
			// if (argumentName == null) throw new ArgumentNullException("argumentName");
			return new CodeConditionStatement(
				new CodeBinaryOperatorExpression(
					new CodeArgumentReferenceExpression(argumentName),
					CodeBinaryOperatorType.ValueEquality,
					new CodePrimitiveExpression(null)
				),
				new CodeThrowExceptionStatement(
					new CodeObjectCreateExpression(
						typeof(ArgumentNullException).Name,
						new CodePrimitiveExpression(argumentName)
					)
				)
			);
		}
	}
}
